/*
 * Copyright (C) 2009, 2010 Jayway AB
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.simpligility.maven.plugins.android;

import com.android.SdkConstants;
import com.android.annotations.Nullable;
import com.android.repository.Revision;
import com.android.sdklib.AndroidTargetHash;
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.BuildToolInfo;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.repository.AndroidSdkHandler;
import com.android.sdklib.repository.targets.AndroidTargetManager;
import org.apache.maven.plugin.MojoExecutionException;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;

/**
 * Represents an Android SDK.
 * 
 * @author hugo.josefson@jayway.com
 * @author Manfred Moser - manfred@simpligility.com
 */
public class AndroidSdk
{
    /**
     * the default API level for the SDK used as a fall back if none is supplied, 
     * should ideally point to the latest available version
     */
    private static final String DEFAULT_ANDROID_API_LEVEL = "23";
    /**
     * property file in each platform folder with details about platform.
     */
    private static final String SOURCE_PROPERTIES_FILENAME = "source.properties";
    /**
     * property name for the sdk tools revision in sdk/tools/lib source.properties
     */
    private static final String SDK_TOOLS_REVISION_PROPERTY = "Pkg.Revision";

    /**
     * folder name for the sdk sub folder that contains the different platform versions.
     */
    private static final String PLATFORMS_FOLDER_NAME = "platforms";

    private static final String BIN_FOLDER_NAME_IN_TOOLS = "bin";

    private static final String PARAMETER_MESSAGE = "Please provide a proper Android SDK directory path as "
            + "configuration parameter <sdk><path>...</path></sdk> in the plugin <configuration/>. As an alternative,"
            + " you may add the parameter to commandline: -Dandroid.sdk.path=... or set environment variable "
            + AbstractAndroidMojo.ENV_ANDROID_HOME + ".";

    private final File sdkPath;
    private File platformToolsPath;
    private File toolsPath;

    private final IAndroidTarget androidTarget;
    private AndroidSdkHandler sdkManager;
    private int sdkMajorVersion;
    private String buildToolsVersion;
    private ProgressIndicatorImpl progressIndicator;

    public AndroidSdk( File sdkPath, String apiLevel )
    {
        this( sdkPath, apiLevel, null );
    }

    public AndroidSdk( File sdkPath, String apiLevel, @Nullable String buildToolsVersion )
    {
        this.sdkPath = sdkPath;
        this.buildToolsVersion = buildToolsVersion;
        this.progressIndicator = new ProgressIndicatorImpl();

        if ( sdkPath != null )
        {
            sdkManager = AndroidSdkHandler.getInstance( sdkPath );
            platformToolsPath = new File( sdkPath, SdkConstants.FD_PLATFORM_TOOLS );
            toolsPath = new File( sdkPath, SdkConstants.FD_TOOLS );

            if ( sdkManager == null )
            {
                throw invalidSdkException( sdkPath, apiLevel );
            }
        }
        loadSDKToolsMajorVersion();

        if ( apiLevel == null )
        {
            apiLevel = DEFAULT_ANDROID_API_LEVEL;
        }

        androidTarget = findPlatformByApiLevel( apiLevel );
        if ( androidTarget == null )
        {
            throw invalidSdkException( sdkPath, apiLevel );
        }
    }

    private InvalidSdkException invalidSdkException( File sdkPath, String platformOrApiLevel )
    {
        throw new InvalidSdkException( "Invalid SDK: Platform/API level " + platformOrApiLevel
                + " not available. This command should give you all you need:\n" + sdkPath.getAbsolutePath()
                + File.separator + "tools" + File.separator + "android update sdk --no-ui --obsolete --force" );
    }

    private IAndroidTarget findPlatformByApiLevel( String apiLevel )
    {
        // try find by api level first
        AndroidVersion version = null;
        try
        {
            version = new AndroidVersion( apiLevel );
            String hashString = AndroidTargetHash.getPlatformHashString( version );
            IAndroidTarget target = sdkManager.getAndroidTargetManager( progressIndicator )
                    .getTargetFromHashString( hashString, progressIndicator );

            // SdkManager may return a non-null IAndroidTarget that references nothing.
            // I suspect it points to an SDK that has been removed.
            if ( target != null && target.getLocation() != null )
            {
                return target;
            }
        }
        catch ( AndroidVersion.AndroidVersionException ignore )
        {
            throw new InvalidSdkException( "Error AndroidVersion: " + ignore.getMessage() );
        }

        // fallback to searching for platform on standard Android platforms (isPlatform() is true)
        for ( IAndroidTarget t: sdkManager.getAndroidTargetManager( null ).getTargets( null ) )
        {
            if ( t.isPlatform() && apiLevel.equals( t.getVersionName() ) )
            {
                return t;
            }
        }
        return null;
    }

    private void assertPathIsDirectory( final File path )
    {
        if ( path == null )
        {
            throw new InvalidSdkException( PARAMETER_MESSAGE );
        }
        if ( !path.isDirectory() )
        {
            throw new InvalidSdkException( "Path \"" + path + "\" is not a directory. " + PARAMETER_MESSAGE );
        }
    }

    /**
     * Get the aapt tool path.
     *
     * @return
     */
    public String getAaptPath()
    {
        return getPathForBuildTool( BuildToolInfo.PathId.AAPT );
    }

    /**
     * Get the aild tool path
     * @return
     */
    public String getAidlPath()
    {
        return getPathForBuildTool( BuildToolInfo.PathId.AIDL );
    }

    /**
     * Get the path for dx.jar
     * @return
     */
    public String getDxJarPath()
    {
        return getPathForBuildTool( BuildToolInfo.PathId.DX_JAR );
    }

    /**
     * @return the path to the dx.jar
     */
    public String getD8JarPath()
    {
        final File pathToDexJar = new File( getPathForBuildTool( BuildToolInfo.PathId.DX_JAR ) );
        final File pathToD8Jar = new File( pathToDexJar.getParent(), "d8.jar" );
        return pathToD8Jar.getAbsolutePath();
    }

    /**
     * Get the path for proguard.jar
     * @return
     */
    public String getProguardJarPath()
    {
        File directory = new File( getToolsPath(), "proguard" + File.separator + "lib" + File.separator );
        File proguardJar = new File( directory, "proguard.jar" );
        if ( proguardJar.exists() ) 
        {
            return proguardJar.getAbsolutePath();
        }
        throw new InvalidSdkException( "Cannot find " + proguardJar );
    }
    
    /**
     * Get the path for shrinkedAndroid.jar
     * @return
     */
    public String getShrinkedAndroidJarPath()
    {
        File shrinkedAndroidJar = new File( getBuildToolsLibDirectoryPath(), "shrinkedAndroid.jar" );
        if ( shrinkedAndroidJar.exists() ) 
        {
            return shrinkedAndroidJar.getAbsolutePath();
        }
        throw new InvalidSdkException( "Cannot find " + shrinkedAndroidJar );
    }
    
    /**
     * Get the path for build-tools lib directory
     * @return
     */
    public String getBuildToolsLibDirectoryPath()
    {
        File buildToolsLib = new File( getBuildToolInfo().getLocation(), "lib" );
        if ( buildToolsLib.exists() ) 
        {
            return buildToolsLib.getAbsolutePath();
        }
        throw new InvalidSdkException( "Cannot find " + buildToolsLib );
    }
    
    /**
     * Get the path for mainDexClasses.rules
     * @return
     */
    public String getMainDexClassesRulesPath()
    {
        File mainDexClassesRules = new File( getBuildToolInfo().getLocation(),
                "mainDexClasses.rules" );
        if ( mainDexClassesRules.exists() ) 
        {
            return mainDexClassesRules.getAbsolutePath();
        }
        throw new InvalidSdkException( "Cannot find " + mainDexClassesRules );
    }

    public void assertThatBuildToolsVersionIsAtLeast( String version, String feature ) 
            throws InvalidSdkException, NumberFormatException 
    {
        if ( getBuildToolInfo().getRevision().
                compareTo( Revision.parseRevision( version ) ) < 0 )
        {
            throw new InvalidSdkException( "Version of build tools must be at least " 
                    + version + " for " + feature + " to work" );
        }
    }
    
    /**
     * Get the android debug tool path (adb).
     *
     * @return
     */
    public String getAdbPath()
    {
        return getPathForPlatformTool( SdkConstants.FN_ADB );
    }

    /**
     * Get the android zipalign path.
     *
     * @return
     */
    public String getZipalignPath()
    {
        return getPathForBuildTool( BuildToolInfo.PathId.ZIP_ALIGN );
    }

    /**
     * Get the android lint path.
     * 
     * @return
     */
    public String getLintPath()
    {
        return getPathForTool( BIN_FOLDER_NAME_IN_TOOLS + "/" + "lint" + ext( ".bat", "" ) );
    }

    /**
     * Get the android monkey runner path.
     * 
     * @return
     */
    public String getMonkeyRunnerPath()
    {
        return getPathForTool( BIN_FOLDER_NAME_IN_TOOLS + "/" + "monkeyrunner" + ext( ".bat", "" ) );
    }

    /**
     * Get the apkbuilder path.
     *
     * @return
     */
    public String getApkBuilderPath()
    {
        return getPathForTool( "apkbuilder" + ext( ".bat", "" ) );
    }

    /**
     * Get the android tool path.
     *
     * @return
     */
    public String getAndroidPath()
    {
        String cmd = "android";
        String ext = SdkConstants.currentPlatform() == 2 ? ".bat" : "";

        return getPathForTool( cmd + ext );
    }

    /**
     * Get the path to the tools directory.
     * @return
     */
    public File getToolsPath()
    {
        return toolsPath;
    }

    private String getPathForBuildTool( BuildToolInfo.PathId pathId )
    {
        return getBuildToolInfo().getPath( pathId );
    }
    
    private BuildToolInfo getBuildToolInfo()
    {
        //First we use the build tools specified in the pom file
        if ( buildToolsVersion != null && !buildToolsVersion.equals( "" ) )
        {
            BuildToolInfo buildToolInfo = sdkManager.getBuildToolInfo( Revision.parseRevision( buildToolsVersion ),
                    progressIndicator );
            if ( buildToolInfo != null )
            {
                return buildToolInfo;
            }
            //Since we cannot find the build tool specified by the user we make it fail
            // instead of using the latest build tool version
            throw new InvalidSdkException( "Invalid SDK: Build-tools " + buildToolsVersion + " not found."
                    + " Check your Android SDK to install the build tools " + buildToolsVersion );
        }

        if ( androidTarget != null )
        {
            BuildToolInfo buildToolInfo = androidTarget.getBuildToolInfo();
            if ( buildToolInfo != null ) 
            {
                return buildToolInfo;
            }
        }
        // if no valid target is defined, or it has no build tools installed, try to use the latest
        BuildToolInfo latestBuildToolInfo = sdkManager.getLatestBuildTool( progressIndicator, true );
        if ( latestBuildToolInfo == null )
        {
            throw new InvalidSdkException( "Invalid SDK: Build-tools not found. Check the content of '" 
                + sdkPath.getAbsolutePath() + File.separator + "build-tools', or run '" 
                + sdkPath.getAbsolutePath() + File.separator + "tools" + File.separator 
                + "android sdk' to install them" );
        }
        return latestBuildToolInfo;
    }

    private String getPathForPlatformTool( String tool )
    {
        return new File( platformToolsPath, tool ).getAbsolutePath();
    }

    private String getPathForTool( String tool )
    {
        return new File( toolsPath, tool ).getAbsolutePath();
    }

    private static String ext( String windowsExtension, String nonWindowsExtension )
    {
        if ( SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS )
        {
            return windowsExtension;
        }
        else
        {
            return nonWindowsExtension;
        }
    }

    /**
     * Returns the complete path for <code>framework.aidl</code>, based on this SDK.
     * 
     * @return the complete path as a <code>String</code>, including the filename.
     */
    public String getPathForFrameworkAidl()
    {
        return androidTarget.getPath( IAndroidTarget.ANDROID_AIDL );
    }

    /**
     * Resolves the android.jar from this SDK.
     * 
     * @return a <code>File</code> pointing to the android.jar file.
     * @throws org.apache.maven.plugin.MojoExecutionException
     *             if the file can not be resolved.
     */
    public File getAndroidJar() throws MojoExecutionException
    {
        final String androidJarPath = androidTarget.getPath( IAndroidTarget.ANDROID_JAR );
        if ( androidJarPath == null )
        {
            throw new MojoExecutionException( "No AndroidJar found for " + androidTarget.getLocation() );
        }
        return new File ( androidJarPath );
    }
  
    /**
     * Resolves the path for this SDK.
     * 
     * @return a <code>File</code> pointing to the SDk Directory.
     * @throws org.apache.maven.plugin.MojoExecutionException
     *             if the file can not be resolved.
     */
    public File getSdkPath() throws MojoExecutionException
    {
        if ( sdkPath.exists() )
        {
            return sdkPath;
        }
        throw new MojoExecutionException( "Can't find the SDK directory : " + sdkPath.getAbsolutePath() );
    }

    /**
     * This method returns the previously specified version. However, if none have been specified it returns the
     * "latest" version.
     */
    public File getPlatform()
    {
        assertPathIsDirectory( sdkPath );

        final File platformsDirectory = new File( sdkPath, PLATFORMS_FOLDER_NAME );
        assertPathIsDirectory( platformsDirectory );

        final File platformDirectory;
        if ( androidTarget == null )
        {
            IAndroidTarget latestTarget = null;
            AndroidTargetManager targetManager = sdkManager.getAndroidTargetManager( progressIndicator );
            for ( IAndroidTarget target: targetManager.getTargets( progressIndicator ) )
            {
                if ( target.isPlatform() )
                {
                    if ( latestTarget == null
                            || target.getVersion().getApiLevel() > latestTarget.getVersion().getApiLevel() )
                    {
                        latestTarget = target;
                    }
                }
            }
            platformDirectory = new File ( latestTarget.getLocation() );
        }
        else
        {
            platformDirectory = new File( androidTarget.getLocation() );
        }
        assertPathIsDirectory( platformDirectory );
        return platformDirectory;
    }

    /**
     * Loads the SDK Tools version
     */
    private void loadSDKToolsMajorVersion()
    {
        File propFile = new File( sdkPath, "tools/" + SOURCE_PROPERTIES_FILENAME );
        Properties properties = new Properties();
        try
        {
            properties.load( new FileInputStream( propFile ) );
        }
        catch ( IOException e )
        {
            throw new InvalidSdkException( "Error reading " + propFile.getAbsoluteFile() );
        }

        if ( properties.containsKey( SDK_TOOLS_REVISION_PROPERTY ) )
        {
            try
            {
                String versionString = properties.getProperty( SDK_TOOLS_REVISION_PROPERTY );
                String majorVersion;
                if ( versionString.matches( ".*[\\.| ].*" ) )
                {
                    String[] versions = versionString.split( "[\\.| ]" );
                    majorVersion = versions[ 0 ];
                }
                else
                {
                    majorVersion = versionString;
                }
                sdkMajorVersion = Integer.parseInt( majorVersion );
            }
            catch ( NumberFormatException e )
            {
                throw new InvalidSdkException( "Error - The property '" + SDK_TOOLS_REVISION_PROPERTY
                        + "' in the SDK source.properties file  number is not an Integer: "
                        + properties.getProperty( SDK_TOOLS_REVISION_PROPERTY ) );
            }
        }
    }

    /**
     * Returns the version of the SDK Tools.
     * 
     * @return
     */
    public int getSdkMajorVersion()
    {
        return sdkMajorVersion;
    }
}
